import fs from 'fs'
import path from 'path'
import config from '@/config'
import fs_promises from 'fs/promises'
import { nanoid } from 'nanoid'
import { globalLogger } from '@/log'
import { sortPaths } from '@/utils/path'
import { createProjectLogger } from '@/log'
import { hasOwnProperty } from '@/utils/common'
import { mkdirIfNotExistSync, writeEmptyFile } from '@/utils/fs'
import { matchPasswordHashRegExp, verify } from '@/utils/password'
import type { Logger } from 'winston'
import type { ServerDebugConfig } from '.'
import type { ServerConnect } from '@/server/connect'
import type { ParsedServerProjectConfig } from '@/types/configFile'
import type { FileStructureDiffResult, HashMap } from '@/utils/fileStructure'

export class Project {
  constructor(config: ParsedServerProjectConfig, debugConfig: ServerDebugConfig) {
    this.config = config
    this.name = config.name
    this.debugConfig = debugConfig

    this.cacheInit()

    this.logger = createProjectLogger({
      dir: config.cache.log
    })
  }

  readonly name: string
  readonly config: ParsedServerProjectConfig
  readonly logger: Logger
  readonly debugConfig: ServerDebugConfig

  protected connect: ServerConnect | null = null

  /**
   * 项目是否处于连接中
   */
  get connecting() {
    return !!this.connect
  }

  /**
   * 设置连接对象
   */
  setConnect(connect: ServerConnect) {
    this.connect = connect
    connect.once('end', () => {
      this.connect = null
    })
  }

  // 缓存初始化
  private async cacheInit() {
    try {
      mkdirIfNotExistSync(this.config.cache.root)
      mkdirIfNotExistSync(this.config.cache.log)
      mkdirIfNotExistSync(this.config.cache.temp)
      mkdirIfNotExistSync(this.config.cache.backup)
      this.hashMapInit()
    } catch (error) {
      globalLogger.server.error(error)
      throw error
    }
  }

  private hashMapInit() {
    if (this.hashMapExist()) return
    this.setHashMap({}).catch((err) => {
      throw this.loggerError(err)
    })
  }

  async setHashMap(hashMap: HashMap) {
    await fs_promises.writeFile(
      this.config.cache.hashMap,
      JSON.stringify(hashMap, null, config.cache.hashMapSpace),
      {
        encoding: config.cache.hashMapEncoding
      }
    )
  }

  async getHashMap() {
    const hashMap = await fs_promises.readFile(this.config.cache.hashMap, {
      encoding: config.cache.hashMapEncoding
    })
    return JSON.parse(hashMap.toString()) as HashMap
  }

  hashMapExist() {
    return fs.existsSync(this.config.cache.hashMap)
  }

  protected loggerError(err: Error | string) {
    this.logger.error(err)
    return err
  }

  joinTempPath(name: string) {
    return path.join(this.config.cache.temp, name)
  }

  /**
   * 写入临时文件
   * @param name 临时文件名
   * @param data 文件数据
   */
  async writeTemp(name = nanoid(), data: Buffer) {
    await fs_promises.writeFile(this.joinTempPath(name), data)
  }

  /**
   * 创建临时文件写入流
   * @param name 临时文件名
   */
  createTempWriteStream(name = nanoid()) {
    const writeStream = fs.createWriteStream(this.joinTempPath(name))

    writeStream.on('error', (err) => {
      this.loggerError(err)
    })

    return writeStream
  }

  /**
   * 以追加的方式写入临时文件
   */
  appendTemp(name = nanoid(), data: Buffer) {
    return fs_promises.appendFile(this.joinTempPath(name), data)
  }

  /**
   * 删除临时文件
   * @param name 临时文件名
   */
  async remoteTemp(name: string) {
    return await fs_promises.rm(this.joinTempPath(name))
  }

  /**
   * 临时文件是否存在
   * @param name 临时文件名
   */
  tempExist(name: string) {
    return fs.existsSync(this.joinTempPath(name))
  }

  /**
   * 删除所有临时文件
   */
  async remoteTempAll() {
    return await fs_promises.rm(this.config.cache.temp, { recursive: true })
  }

  /**
   * 从临时目录复制文件到目标路径
   * @param name 临时文件名
   * @param to 目标路径
   * @param mode `fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE`
   */
  async copyFileFromTemp(name: string, to: string, mode?: number) {
    return await fs_promises.copyFile(path.join(this.config.cache.temp, name), to, mode)
  }

  /**
   * 从临时目录移动文件到目标路径
   * @param name 临时文件名
   * @param to 目标路径
   */
  async moveFileFromTemp(name: string, to: string) {
    return await fs_promises.rename(path.join(this.config.cache.temp, name), to)
  }

  /**
   * 验证密码
   */
  verifyPassword(password: string) {
    return this.config.password.some((item) => {
      if (matchPasswordHashRegExp.test(item)) {
        return verify(password, item)
      } else {
        return item === password
      }
    })
  }

  async updateFile(diffResult: FileStructureDiffResult) {
    const addDirs = sortPaths(
      Object.keys(diffResult.added).filter((k) => diffResult.added[k] === 'dir'),
      'asc'
    )
    const writeObj = {
      ...diffResult.sended,
      ...diffResult.sended_duplicate,
      ...diffResult.sended_empty
    }
    const writePaths = sortPaths(Object.keys(writeObj), 'asc')
    const removedPaths = sortPaths(Object.keys(diffResult.removed))

    // 创建新增的目录
    for (const dir of addDirs) {
      const _path = path.join(this.config.path, dir)
      if (fs.existsSync(_path)) continue
      await fs_promises.mkdir(_path)
    }

    // 写入新增和变化的文件
    for (const writePath of writePaths) {
      const filePath = path.join(this.config.path, writePath)
      const value = writeObj[writePath]
      if (value === 'empty') {
        // 空文件处理
        await writeEmptyFile(filePath)
        continue
      }
      if (!this.tempExist(value)) {
        throw new Error(`temp file ${value} not exist`)
      }
      await this.copyFileFromTemp(value, filePath)
    }

    // 删除目录和文件
    for (const pathItem of removedPaths) {
      const _path = path.join(this.config.path, pathItem)
      if (!fs.existsSync(_path)) continue
      await fs_promises.rm(_path)
    }

    if (!this.debugConfig.notUpdateHashMap) await this.updateHashMapFromDiffResult(diffResult)

    await this.clearTempFilesFromDiffResult(diffResult)
  }

  /**
   * 从 DiffResult 更新 HashMap
   */
  protected async updateHashMapFromDiffResult(diffResult: FileStructureDiffResult) {
    const hashMap = await this.getHashMap()
    const splitPath = (v: string) => path.normalize(v).split(path.sep)
    let hasChanged = false

    for (const _path in diffResult.added) {
      const paths = splitPath(_path)
      let temp = hashMap
      for (let i = 0; i < paths.length; i++) {
        const key = paths[i]
        if (i === paths.length - 1) {
          hasChanged = true
          temp[key] = diffResult.added[_path] === 'dir' ? {} : diffResult.added[_path]
          break
        }
        if (hasOwnProperty(temp, key)) {
          // @ts-ignore
          temp = temp[key]
        } else {
          temp[key] = {}
        }
      }
    }

    for (const _path in diffResult.changed) {
      const paths = splitPath(_path)
      let temp = hashMap
      for (let i = 0; i < paths.length; i++) {
        const key = paths[i]
        if (i === paths.length - 1) {
          hasChanged = true
          temp[key] = diffResult.changed[_path]
          break
        }
        if (hasOwnProperty(temp, key)) {
          // @ts-ignore
          temp = temp[key]
        } else {
          temp[key] = {}
        }
      }
    }

    for (const _path in diffResult.removed) {
      const paths = splitPath(_path)
      let temp = hashMap
      for (let i = 0; i < paths.length; i++) {
        const key = paths[i]
        if (i === paths.length - 1) {
          hasChanged = true
          delete temp[key]
          break
        }
        if (hasOwnProperty(temp, key)) {
          // @ts-ignore
          temp = temp[key]
        } else {
          break
        }
      }
    }

    if (hasChanged) {
      await this.setHashMap(hashMap)
    }
  }

  /**
   * 从 DiffResult 清除临时文件
   */
  protected async clearTempFilesFromDiffResult(diffResult: FileStructureDiffResult) {
    for (const key in diffResult.sended) {
      await this.remoteTemp(diffResult.sended[key])
    }
  }
}
